package org.gpp.proj1.logic.profile;

import java.io.Serializable;
import java.security.InvalidParameterException;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import org.gpp.proj1.logic.metadata.PartCategory;
import org.gpp.proj1.logic.metadata.PartEntry;
import org.gpp.proj1.logic.metadata.RobotEntry;
import org.gpp.proj1.logic.metadata.RobotPartCategory;

/**
 * Stores players' parts, robot definitions, statistics and cash
 * 
 * @author Gustavo Almeida
 * @author Paolo Victor, paolovictor@gmail.com
 */
public class PlayerProfile implements Serializable {

	private static final long serialVersionUID = 4670738129994547254L;

	/**
	 * The players' initial cash
	 */
	private static final int DEFAULT_INITIAL_CASH = 5000;

	/**
	 * The player's identification, which will be displayed on the 
	 * lobby and game screen
	 */
	private String name;
	
	/**
	 * The player's statistics (wins, losses, abandons)
	 */
	private Statistics stats;

	/**
	 * Maps category to part sets
	 */
	private Map<PartCategory, Map<PartEntry, Integer>> parts;

	/**
	 * The player's cash. Can be used to buy robot parts
	 */
	private int cash;
	
	/**
	 * The player's robot collection
	 */
	private final Map<String, RobotEntry> robots;

	/**
	 * The parts that may be equipped
	 */
	private Map<PartCategory, Map<PartEntry, Integer>> availableParts;
	
	/**
	 * @param name The player name
	 * @param stats The player's statistics
	 * @param parts The player's total parts
	 * @param availableParts The parts available for robot assignind
	 * @param robots The player's robots
	 * @param cash The player's cash
	 */
	public PlayerProfile( String name, Statistics stats,
			Map<PartCategory, Map<PartEntry, Integer>> parts,
			Map<PartCategory, Map<PartEntry, Integer>> availableParts,
			Map<String, RobotEntry> robots, int cash ) {

		if( name == null || name.trim().equals( "" )) {
			throw new IllegalArgumentException( "Invalid name: \"" + name + "\"" );
		}
		
		if( stats == null ) {
			throw new IllegalArgumentException( "Invalid statistics entry" );
		}

		this.name = name;
		this.stats = stats;
		this.parts = parts;
		this.availableParts = availableParts;
		this.cash = cash;
		this.robots = robots;
		
		for( PartCategory category : PartCategory.values() ) {
			parts.put( category, new HashMap<PartEntry,Integer>() );
			availableParts.put( category, new HashMap<PartEntry,Integer>() );
		}
	}
	
	/**
	 * @param name The player's name. The parts are initialized to empty and
	 * the cash to the default value
	 */
	public PlayerProfile( String name ) {
		
		this( name, new Statistics(),
			  new HashMap<PartCategory, Map<PartEntry,Integer>>(),
			  new HashMap<PartCategory, Map<PartEntry,Integer>>(),
			  new HashMap<String, RobotEntry>(), PlayerProfile.DEFAULT_INITIAL_CASH );
	}

	/**
	 * Add a part of a given category to the total part set
	 * 
	 * @param partCategory
	 * @param part
	 */
	public void addPart( PartCategory partCategory, PartEntry part ) {
		
		if( partCategory == null ) {
			throw new InvalidParameterException( "Invalid category: \"" + partCategory + "\"" );
		}
		if( part == null ) {
			throw new InvalidParameterException( "Invalid part: \"" + part + "\"" );
		}
		
		storePart( parts, partCategory, part );
		storePart( availableParts, partCategory, part );
	}
	
	public int getCash() {
		
		return cash;
	}

	public String getName() {
		
		return name;
	}

	public Statistics getStats() {
		
		return stats;
	}
	
	/**
	 * Debits a given amount of cash from the player profile
	 * @param amount
	 * @throws UnsufficientFundsException If there's not enough cash be removed
	 */
	public synchronized void debit( int amount ) throws UnsufficientFundsException {

		if( amount > this.cash ) {
			throw new UnsufficientFundsException( "Not enough cash to debit" );
		}
		
		this.cash -= amount;
	}
	
	/**
	 * @param robotName
	 * @return <code>true</code> if the profile contains the robot
	 */
	public boolean existsRobot( String robotName ) {
		return this.robots.containsKey( robotName );
	}

	/**
	 * @param robotName
	 * @return The robot entry for the given robot name
	 */
	public RobotEntry getRobot( String robotName ) {
		
		if( robotName == null || robotName.trim().equals("") ||
			!this.robots.containsKey( robotName ) ) {
    		throw new InvalidParameterException( "Invalid robot name" );
    	}

		return this.robots.get( robotName );
	}
	
	/**
	 * @param category
	 * @return The parts that can be equipped from a given part category
	 */
	public Map<PartEntry, Integer> getAvailableParts( PartCategory category ) {
		
		return new HashMap<PartEntry, Integer>( this.availableParts.get( category ) );
	}

	/**
	 * Adds a part to a part map
	 * 
	 * @param partMap
	 * @param partCategory
	 * @param partName
	 */
	private void storePart( Map<PartCategory, Map<PartEntry, Integer>> partMap,			
			PartCategory partCategory, PartEntry part ) {
		Map<PartEntry, Integer> partSet = partMap.get( partCategory );
		
		// If the part doesn't exist, create it. Add otherwise
		if( partSet.containsKey( part ) ) {
			Integer quantity = partSet.get( part );
			quantity++;
			
			partSet.put( part, quantity );
		} else {
			partSet.put( part, 1 );
		}
	}
	
	/**
	 * Removes a part from a part map. If removes the key
	 * when there are no other parts of the same type left 
	 * 
	 * @param partMap
	 * @param partCategory
	 * @param part
	 */
	private void removePart( Map<PartCategory, Map<PartEntry, Integer>> partMap,			
			PartCategory partCategory, PartEntry part ) {
		Map<PartEntry, Integer> partSet = partMap.get( partCategory );

		// If there are more than 1 parts, update count,
		// remove part otherwise
		if( partSet.containsKey( part ) ) {
			Integer quantity = partSet.get( part );

			if( quantity == 1 ) {			
				partSet.remove( part );
			} else {
				quantity--;			
				partSet.put( part, quantity );
			}
		} else {
			throw new InvalidParameterException( "Unavailable part" );
		}
	}

	/**
	 * Saves a robot entry, updating an old one if it exists
	 * @param newEntry
	 */
	public void saveRobot(RobotEntry oldEntry, RobotEntry newEntry) {
		// If robot already exists, put back parts
		if( oldEntry != null && existsRobot( oldEntry.getRobotName() ) ) {
			returnOldParts( this.robots.get( oldEntry.getRobotName() ) );
		}
		
		this.robots.put( newEntry.getRobotName(), newEntry );
		
		// Assigning parts
		removePart( availableParts, PartCategory.HEAD, newEntry.getPart( RobotPartCategory.HEAD ) );
		removePart( availableParts, PartCategory.TORSO, newEntry.getPart( RobotPartCategory.TORSO ) );
		removePart( availableParts, PartCategory.LEGS, newEntry.getPart( RobotPartCategory.LEGS ) );
		removePart( availableParts, PartCategory.ARM, newEntry.getPart( RobotPartCategory.ARM_LEFT ) );
		removePart( availableParts, PartCategory.ARM, newEntry.getPart( RobotPartCategory.ARM_RIGHT ) );
	}

	/**
	 * Returns a robot's parts to the available parts map
	 * @param old
	 */
	private void returnOldParts(RobotEntry old) {
		storePart( availableParts, PartCategory.HEAD, old.getPart( RobotPartCategory.HEAD ) );
		storePart( availableParts, PartCategory.TORSO, old.getPart( RobotPartCategory.TORSO ) );
		storePart( availableParts, PartCategory.LEGS, old.getPart( RobotPartCategory.LEGS ) );
		storePart( availableParts, PartCategory.ARM, old.getPart( RobotPartCategory.ARM_LEFT ) );
		storePart( availableParts, PartCategory.ARM, old.getPart( RobotPartCategory.ARM_RIGHT ) );
	}

	/**
	 * @return The player's robots
	 */
	public List<RobotEntry> getPlayerRobots() {

		return new LinkedList<RobotEntry>( this.robots.values() );
	}

	/**
	 * Deletes the robot from the player profile
	 * @param robotName
	 */
	public void deleteRobot(String robotName) {
		
		returnOldParts( this.robots.get( robotName ) );
		this.robots.remove( robotName );
	}
}
